﻿/*
 * Copyright 2011 Edward L. Stauff.                     Contact: <EdStauff@gmail.com>.
 * 
 * This file, "CmdLineValueParser.cs", is part of the "Stauffware.Common.Console" class 
 * library.  "Stauffware.Common" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * "Stauffware.Common".  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using Stauffware.Common.Core;
using Stauffware.Common.Time;

namespace Stauffware.Common.Console
{
	/// <summary>
	/// A parser for command-line values of a particular type.
	/// </summary>
	public interface ICmdLineValueParser
	{
		/// <summary> Gets the value type. </summary>
		Type ValueType { get; }
		
		/// <summary> 
		/// Gets a string that describes the command-line syntax.
		/// </summary>
		string Help { get; }
		
		/// <summary>
		/// Parses the value of one command-line parameter.
		/// </summary>
		/// <param name="valueStr">the string to be parsed</param>
		/// <param name="argDescr">
		///		Describes the parameter for the value being parsed (for error reporting).
		/// </param>
		/// <returns>the parsed value</returns>
		object Parse (string valueStr, string argDescr);
	}

	//#################################################################################
	/// <summary>
	/// Parses a parameter value from a string and returns the parsed result.
	/// Throws a CmdLineSyntaxError if the parse fails.
	/// </summary>
	/// <param name="valueStr">the value string to be parsed</param>
	/// <param name="argDescr">
	///		Describes the parameter for the value being parsed (for error reporting).
	/// </param>
	/// <returns>the parsed result</returns>

	public delegate object CmdLineValueParserDlgt (string valueStr, string argDescr);

	//#################################################################################
	/// <summary>
	/// Provides type-specific parsers for command-line parameter values.
	/// </summary>

	public class ValueParserFactory
	{
		private static DateTimeParser DateTimeParser;
		private static TimeSpanParser TimeSpanParser;

		private Dictionary<Type, ICmdLineValueParser> AllParsers = 
										new Dictionary<Type, ICmdLineValueParser>();

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns a parser for values of the given type.
		/// </summary>
		/// <param name="valueType">the output type of the parser </param>
		/// <returns>the parser, or null if one couldn't be created</returns>

		public ICmdLineValueParser GetParser (Type valueType)
		{
			ICmdLineValueParser parser;
			if (!AllParsers.TryGetValue(valueType, out parser))
			{
				parser = CreateParser(valueType);
				AllParsers.Add(valueType, parser);
			}
			return parser;
		}

		//-----------------------------------------------------------------------------

		private ICmdLineValueParser CreateParser (Type valueType)
		{
			//if (valueType.IsNullable())
			//{
			//    Type[] gTypes = valueType.GetGenericArguments();
			//    Assert.Condition(gTypes.Length == 1);

			//    // I'm not at all sure that handling nullable types won't take additional effort.
			//    valueType = gTypes[0];
			//}

			// See if it's a specific type we know how to handle.

			if (valueType == typeof(string))
				return new CmdLineValueParser(valueType, ParseString, 
												"(recognizes C#-style escapes)");

			if (valueType == typeof(bool))
				return new CmdLineValueParser(valueType, ParseBool, 
							"\"true\"  | \"false\"  | \"yes\" | \"no\"  | \"on\"  | \"off\"");

			if (valueType.IsEnum)
				return new EnumValueParser(valueType);

			if (valueType == typeof(DateTime))
			{
				if (DateTimeParser == null)
					DateTimeParser = new DateTimeParser(DateTimeParseOptions.ParseEntireInput | 
														DateTimeParseOptions.AllowWhiteSpace);
				return new CmdLineValueParser(valueType, ParseDateTime, null);
			}

			if (valueType == typeof(TimeSpan))
			{
				if (TimeSpanParser == null)
					TimeSpanParser = new TimeSpanParser(TimeSpanParseOptions.ParseEntireInput | 
														TimeSpanParseOptions.AllowWhiteSpace);
				return new CmdLineValueParser(valueType, ParseTimeSpan, null);
			}

			// See if the type has a static "Parse" method that takes a single string parameter.

			MethodInfo mi = valueType.GetMethod("Parse", BindingFlags.Static | BindingFlags.Public,
												null, ParseMethodArgTypes, null);
			if (mi != null)
				return new ParseMethodValueParser(mi);

			return null;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Creates a custom value parser based on information provided by the user
		/// (programmer) in a CmdLineValueParserAttribute.
		/// </summary>
		/// <param name="paramsObj">the object that receives parameter values</param>
		/// <param name="methodName">the unqualified or fully qualified name of the parse method</param>
		/// <returns>a value parser</returns>

		public void AddCustom (object paramsObj, string methodName)
		{
			BindingFlags bFlags = BindingFlags.Public;
			Type type;

			int dot = methodName.LastIndexOf('.');
			if (dot < 0)	// unqualified name
			{
				type = paramsObj.GetType();
				bFlags |= BindingFlags.Instance;
			}
			else    // fully qualified name
			{
				string className = methodName.Substring(0, dot);
				methodName = methodName.Substring(dot+1);
				type = Type.GetType(className);
				Require.NotNull(type, "can't find class \"" + className + "\"");
				bFlags |= BindingFlags.Static;
			}

			MethodInfo mi = type.GetMethod(methodName, bFlags);
			Require.NotNull(mi, "can't find method + \"" + methodName + "\" in class " + type.FullName);

			Delegate dlgt;
			if (dot < 0)
				dlgt = Delegate.CreateDelegate(typeof(CmdLineValueParserDlgt), paramsObj, mi);
			else dlgt = Delegate.CreateDelegate(typeof(CmdLineValueParserDlgt), mi);

			CmdLineValueParser clvp = new CmdLineValueParser(type, (CmdLineValueParserDlgt) dlgt, null);
			this.AllParsers[type] = clvp;
		}

		//-----------------------------------------------------------------------------
		///// <summary>
		///// Creates a parser for values of the given type.
		///// </summary>
		///// <param name="valueType">the output type of the parser </param>
		///// <returns>the new parser (a delegate), or null if one couldn't be created</returns>

		//public static CmdLineValueParserDlgt CreateDlgt (Type valueType)
		//{
		//    if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Nullable<>))
		//    {
		//        Type[] gTypes = valueType.GetGenericArguments();
		//        Assert.Condition(gTypes.Length == 1);

		//        // I'm not at all sure that handling nullable types won't take additional effort.
		//        valueType = gTypes[0];
		//    }

		//    // See if it's a specific type we know how to handle.

		//    if (valueType == typeof(string))
		//        return ParseString;

		//    if (valueType == typeof(bool))
		//        return ParseBool;

		//    if (valueType.IsEnum)
		//        return CreateEnumParser(valueType);

		//    if (valueType == typeof(DateTime))
		//    {
		//        if (DateTimeParser == null)
		//            DateTimeParser = new DateTimeParser(DateTimeParseOptions.ParseEntireInput | 
		//                                                DateTimeParseOptions.AllowWhiteSpace);
		//        return ParseDateTime;
		//    }

		//    if (valueType == typeof(TimeSpan))
		//    {
		//        if (TimeSpanParser == null)
		//            TimeSpanParser = new TimeSpanParser(TimeSpanParseOptions.ParseEntireInput | 
		//                                                TimeSpanParseOptions.AllowWhiteSpace);
		//        return ParseTimeSpan;
		//    }

		//    // See if the type has a static "Parse" method that takes a single string parameter.

		//    MethodInfo mi = valueType.GetMethod("Parse", BindingFlags.Static | BindingFlags.Public,
		//                                    null, ParseMethodArgTypes, null);
		//    if (mi != null)
		//        return (string valueStr, string argDescr) => 
		//            {
		//                try
		//                {
		//                    return mi.Invoke(null, new object[] { valueStr });
		//                }
		//                catch (Exception x)
		//                {
		//                    throw new CmdLineSyntaxError(argDescr + " - " + x.Message, x);
		//                }
		//            };

		//    return null;
		//}

		private static readonly Type[] ParseMethodArgTypes = { typeof(string) };

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns a collection of all the value parsers created so far.
		/// </summary>

		public ICollection<ICmdLineValueParser> GetAll ( )
		{
			return this.AllParsers.Values;
		}

		//-----------------------------------------------------------------------------

		private static object ParseString (string valueStr, string argDescr)
		{
			return valueStr;
		}

		//-----------------------------------------------------------------------------

		private static object ParseBool (string valueStr, string argDescr)
		{
			string valueStrLC = valueStr.ToLower();
			if (valueStrLC == "true" || valueStrLC == "yes") return true;
			if (valueStrLC == "false" || valueStrLC == "no") return false;
			throw new CmdLineSyntaxError(argDescr + 
								" - invalid boolean value: \"" + valueStr + "\"");
		}

		//-----------------------------------------------------------------------------

		private static CmdLineValueParserDlgt CreateEnumParser (Type valueType)
		{
			EnumParseOptions epo = EnumParseOptions.ParseEntireInput | EnumParseOptions.IgnoreCase | 
									EnumParseOptions.AllowAbbreviations | EnumParseOptions.AllowMultipleValues;

			EnumParser eparser = new EnumParser(valueType, epo);

			return (string valueStr, string argDescr) =>
			{
				EnumParse p = eparser.Parse(valueStr);
				if (p.GotValue)
					return p.Value;
				if (p.IsError)
					throw new CmdLineSyntaxError(argDescr + " - " + 
							ErrorUtils.ValueToString(valueStr) + " - " + p.ErrorMessage);
				throw new CmdLineSyntaxError("missing value for " + argDescr);
			};
		}

		//-----------------------------------------------------------------------------

		private static object ParseDateTime (string valueStr, string argDescr)
		{
			DateTimeParse p = DateTimeParser.Parse(valueStr);
			if (p.GotValue)
				return p.Value;
			if (p.IsError)
				throw new CmdLineSyntaxError(argDescr + " - " + 
							ErrorUtils.ValueToString(valueStr) + " - " + p.ErrorMessage);
			throw new CmdLineSyntaxError("missing value for " + argDescr);
		}

		//-----------------------------------------------------------------------------

		private static object ParseTimeSpan (string valueStr, string argDescr)
		{
			TimeSpanParse p = TimeSpanParser.Parse(valueStr);
			if (p.GotValue)
				return p.Value;
			if (p.IsError)
				throw new CmdLineSyntaxError(argDescr + " - " + 
							ErrorUtils.ValueToString(valueStr) + " - " + p.ErrorMessage);
			throw new CmdLineSyntaxError("missing value for " + argDescr);
		}

		//-----------------------------------------------------------------------------
		///// <summary>
		///// Returns a help string for the given type, or null if there isn't any.
		///// </summary>

		//public static string GetHelp (Type type)
		//{
		//    if (type == typeof(System.String))
		//    {
		//        return "(recognizes C#-style escapes)";
		//    }
		//    else if (type == typeof(System.Boolean))
		//    {
		//        return "\"true\"  | \"false\"  | \"yes\" | \"no\"  | \"on\"  | \"off\"";
		//    }
		//    else if (type == typeof(System.SByte) || type == typeof(System.Int16) || 
		//             type == typeof(System.Int32) || type == typeof(System.Int64))
		//    {
		//        return "['+' | '-'] (<decimal digit>... | '#' <hex digit>...)";
		//    }
		//    else if (type == typeof(System.Byte) || type == typeof(System.UInt16) || 
		//             type == typeof(System.UInt32) || type == typeof(System.UInt64))
		//    {
		//        return "<decimal digit>... | '#' <hex digit>...";
		//    }
		//    else if (type == typeof(System.Single) || type == typeof(System.Double) || type == typeof(System.Decimal))
		//    {
		//        return "['+' | '-'] [<digit>] ['.'] [<digit...>] ['E' ['+' | '-'] <digit>...]";
		//    }
		//    else return null;
		//}

		//-----------------------------------------------------------------------------
	}

	//#################################################################################

	internal class CmdLineValueParser : ICmdLineValueParser
	{
		public string Help { get; protected set; }

		public Type ValueType { get; private set; }

		private readonly CmdLineValueParserDlgt ParseMethod;

		public CmdLineValueParser (Type valueType, CmdLineValueParserDlgt parseMethod, string help)
		{
			Require.NotNull(valueType);
			Require.NotNull(parseMethod);
			this.ValueType = valueType;
			this.ParseMethod = parseMethod;
			this.Help = help;
		}

		public object Parse (string valueStr, string argDescr)
		{
			return this.ParseMethod(valueStr, argDescr);
		}

		/// for debugging
		public override string ToString ( )
		{	return this.GetType().Name + ": " + this.ValueType.FullName;  }
	}

	//#################################################################################

	internal class ParseMethodValueParser : ICmdLineValueParser
	{
		public string Help { get; private set; }

		public Type ValueType { get; private set; }

		private readonly MethodInfo MethodInfo;

		//-----------------------------------------------------------------------------

		public ParseMethodValueParser (MethodInfo mi)
		{
			this.MethodInfo = mi;
			this.ValueType = mi.DeclaringType;

			if (this.ValueType == typeof(System.SByte) || 
				this.ValueType == typeof(System.Int16) || 
				this.ValueType == typeof(System.Int32) || 
				this.ValueType == typeof(System.Int64))
			{
				this.Help = "['+' | '-'] (<decimal digit>... | '#' <hex digit>...)";
			}
			else if (this.ValueType == typeof(System.Byte) || 
					 this.ValueType == typeof(System.UInt16) || 
					 this.ValueType == typeof(System.UInt32) || 
					 this.ValueType == typeof(System.UInt64))
			{
				this.Help = "<decimal digit>... | '#' <hex digit>...";
			}
			else if (this.ValueType == typeof(System.Single) || 
					 this.ValueType == typeof(System.Double) || 
					 this.ValueType == typeof(System.Decimal))
			{
				this.Help = "['+' | '-'] [<digit>] ['.'] [<digit...>] ['E' ['+' | '-'] <digit>...]";
			}
		}

		//-----------------------------------------------------------------------------

		public object Parse (string valueStr, string argDescr)
		{
			try
			{
				return this.MethodInfo.Invoke(null, new object[] { valueStr });
			}
			catch (Exception x)
			{
				throw new CmdLineSyntaxError(argDescr + " - " + x.Message, x);
			}
		}

		/// for debugging
		public override string ToString ( )
		{ return this.GetType().Name + ": " + this.ValueType.FullName; }
	}

	//#################################################################################

	internal class EnumValueParser : ICmdLineValueParser
	{
		public string Help { get { return null; } }

		public Type ValueType { get; private set; }

		private const EnumParseOptions EPoptions = 
			EnumParseOptions.ParseEntireInput | EnumParseOptions.IgnoreCase | 
			EnumParseOptions.AllowAbbreviations | EnumParseOptions.AllowMultipleValues;

		private readonly EnumParser EnumParser;

		//-----------------------------------------------------------------------------

		public EnumValueParser (Type enumType)
		{
			this.ValueType = enumType;
			this.EnumParser = new EnumParser(enumType, EPoptions);

			ListString values = new ListString(", ");
			foreach (object value in EnumUtils.GetEnumValues(enumType))
				values.Add(value.ToString());
		}

		//-----------------------------------------------------------------------------

		public object Parse (string valueStr, string argDescr)
		{
			EnumParse p = this.EnumParser.Parse(valueStr);
			if (p.GotValue)
				return p.Value;
			if (p.IsError)
				throw new CmdLineSyntaxError(argDescr + " - " + 
							ErrorUtils.ValueToString(valueStr) + " - " + p.ErrorMessage);
			throw new CmdLineSyntaxError("missing value for " + argDescr);
		}

		/// for debugging
		public override string ToString ( )
		{ return this.GetType().Name + ": " + this.ValueType.FullName; }
	}

	//#################################################################################
}
